package gu.sql2java;

import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Map.Entry;

import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Primitives;

import static com.google.common.base.Preconditions.*;
import static gu.sql2java.Sql2javaSupport.getBytesInBuffer;

/**
 * abstract implementation of {@link BaseBean}
 * @author guyadong
 *
 */
public abstract class BaseRow implements BaseBean,Comparable<BaseRow>, Cloneable {

	protected final RowMetaData metaData;
	private volatile Map<String, Object> mapView;
	protected BaseRow(RowMetaData metaData) {
		this.metaData = checkNotNull(metaData,"metaData is null");
	}
	protected BaseRow() {
		this.metaData = checkNotNull(RowMetaData.getMetaData(getClass()),"metaData is null");
	}
	@Override
	public final boolean isInitialized(String column) {
		return isInitialized(metaData.columnIDOf(column));
	}
	
	@Override
	public boolean beModified() {
		for(int i=0; i<metaData.columnCount; ++i){
			if(isModified(i)){
				return true;
			}
		}
		return false;
	}
	
    @Override
    public boolean isModified(String column){        
        return isModified(metaData.columnIDOf(column));
    }
    @Override
    public boolean isModifiedNested(String nestedName){
        if(null == nestedName){
            return false;
        }
        if(isModified(nestedName)){
            return true;
        }
        String prefix = metaData.tablename + ".";
        if(nestedName.startsWith(prefix)){
            /** remove table name */
            nestedName = nestedName.substring(prefix.length());
        }
        int firstDot = nestedName.indexOf('.');
        if(firstDot > 0){
            /** 0-firstDot is column name */
            return isModified(nestedName.substring(0, firstDot));
        } if(firstDot == 0){
            /** column name is empty */
            return false;
        }else {
            /**  only column name */
            return isModified(nestedName);
        }
    }

    @Override
	public int[] modifiedColumnIDs(){
    	List<Integer> list = Lists.newArrayListWithCapacity(metaData.columnCount);
    	for(int columnID = 0; columnID < metaData.columnCount;++columnID){
    		if(isModified(columnID)){
    			list.add(columnID);	
    		}
    	}
    	return Ints.toArray(list);
    }
    @Override
	public String[] modifiedColumns(){
    	return Iterables.toArray(metaData.columnNamesOf(modifiedColumnIDs()),String.class);
    }
    @Override
	public int modifiedColumnCount(){
    	return modifiedColumnIDs().length;
    }
	@Override
	public final <T> T getValue(String column) {
		return getValue(metaData.columnIDOf(column));
	}

	@Override
	public final <T> T getValueChecked(int columnID) {
		T value = getValue(columnID);
		return Preconditions.checkNotNull(value,"value of columnid %s IS NULL", columnID);
	}
	@Override
	public final <T> T getValueChecked(String column) {
		T value = getValue(column);
		return Preconditions.checkNotNull(value,"value of column %s IS NULL", column);
	}
	@Override
	public final void setValue(String column, Object value) {
		setValue(metaData.columnIDOf(column),value);
	}
    @Override
    public final boolean setValueIfNonNull(String column, Object value) 
    {
    	return setValueIf(value != null,column,value);
    }
    
    @Override
    public final boolean setValueIfNonEqual(String column, Object value) 
    {
       return setValueIf(!Objects.equals(value, getValue(column)),column,value);
    }
    @Override
    public final boolean setValueIf(boolean expression,String column, Object value) 
    {
        if(expression){
            setValue(column,value);
        }
        return expression;
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public <T> T getValue(int columnID)
    {
    	try {
			return (T) metaData.getterMethods.get(columnID).invoke(this);
		} catch (IndexOutOfBoundsException e) {
			return null;
		}catch (Exception e) {
			Throwables.throwIfUnchecked(e);
			throw new RuntimeException(e);
		}
    }
    @SuppressWarnings("unchecked")
	public <T> T getOriginValue(int columnID)
    {
    	try {
    		Method method = MoreObjects.firstNonNull(metaData.readMethods.get(columnID), metaData.getterMethods.get(columnID));
    		return (T)method.invoke(this);
    	} catch (IndexOutOfBoundsException e) {
    		return null;
    	}catch (Exception e) {
    		Throwables.throwIfUnchecked(e);
    		throw new RuntimeException(e);
    	}
    }
    
    /**
     * @param value
     * @param expectType
     * @return {@code null} if none cast happened 
     */
    private Object castValue(Object value,Class<?> expectType){
        Class<?> unwrapType = Primitives.unwrap(expectType);
        if(!expectType.isInstance(value)){
        	if(value instanceof Number && unwrapType.isPrimitive()) {
        		Number num = (Number)value;
        		if(int.class.equals(unwrapType)){
        			return num.intValue();
        		}else if(long.class.equals(unwrapType)){
        			return num.longValue();
        		}else if(float.class.equals(unwrapType)){
        			return num.floatValue();
        		}else if(float.class.equals(unwrapType)){
        			return num.floatValue();
        		}else if(double.class.equals(unwrapType)){
        			return num.doubleValue();
        		}else if(byte.class.equals(unwrapType)){
        			return num.byteValue();
        		}else if(short.class.equals(unwrapType)){
        			return num.shortValue();
        		}
        	} else if(value instanceof byte[] && ByteBuffer.class.equals(expectType)){
    			return ByteBuffer.wrap((byte[])value);
    		}else if(value instanceof ByteBuffer && byte[].class.equals(expectType)){
    			return getBytesInBuffer((ByteBuffer)value);
    		}
        }
        return null;
    }
    private <T> void setValue(Method method,T value) throws IllegalArgumentException
	{
		try {
			method.invoke(this, value);
		} catch ( IllegalArgumentException e) {
			Object v = castValue(value,method.getParameterTypes()[0]);
			if(null != v){
				setValue(method,v);
				return;
			}
			throw e;
		}catch (Exception e) {
			Throwables.throwIfUnchecked(e);
			throw new RuntimeException(e);
		}
	}
	@Override
    public <T> void setValue(int columnID,T value)
    {
    	try {
    		setValue(metaData.setterMethods.get(columnID), value);
		} catch (IndexOutOfBoundsException e) {
			return ;
		}catch ( IllegalArgumentException e) {
			Method writeMethod = metaData.writeMethods.get(columnID);
			if(null != writeMethod) {
				setValue(writeMethod, value);
				return;
			}
		    throw e;
        }
    }
    @Override
    public final <T> boolean setValueIfNonNull(int columnID,T value)
    {
        return setValueIf(value != null,columnID,value);
    }
    @Override
    public final <T> boolean setValueIfNonEqual(int columnID,T value)
    {
        return setValueIf(!Objects.equals(value, getValue(columnID)),columnID,value);
    }
    @Override
    public final <T> boolean setValueIf(boolean expression,int columnID,T value)
    {
        if(expression){
            setValue(columnID,value);
        }
        return expression;
    }
    
    @Override
    public Object[] primaryValues()
    {
        Object[] values = new Object[metaData.primaryKeyCount];
        for(int i = 0; i < values.length; ++i) {
            values[i] = getValue(metaData.primaryKeyIds[i]);
        }
        return values;
    }

    @Override
    public <T> T primaryValue()
    {
        if(metaData.primaryKeyCount != 1) {
            throw new UnsupportedOperationException();
        }
        return getValue(metaData.primaryKeyIds[0]);
    }
    
    @Override
    public Object[] asValueArray(int...columnIds)
    {
		if(null == columnIds || columnIds.length == 0){
			columnIds = metaData.defaultColumnIdList;
		}
        Object[] v = new Object[columnIds.length];
        for(int i=0; i< v.length; ++i){
        	v[i] = getValue(columnIds[i]);
        }
        return v;
    }
	@Override
	public Map<String, Object> asNameValueMap(){
		// double check 
		if(mapView == null){
			synchronized (this) {
				if(mapView == null){
					mapView = new RowMapView(this);
				}
			}
		}
		return mapView;
	}
	@Override
	public Map<String, Object> asNameValueMap(boolean ignoreNull,String ...ignoreColumns){
		return asNameValueMap(ignoreNull, false,null != ignoreColumns ? Arrays.asList(ignoreColumns) : Collections.<String>emptySet());
	}
	@Override
	public Map<String, Object> asNameValueMap(boolean ignoreNull,Iterable<String>ignoreColumns){
		return asNameValueMap(ignoreNull,false,ignoreColumns);
	}
	@Override
	public Map<String, Object> asNameValueMap(boolean ignoreNull,boolean include,String ...includeColumns){
		return asNameValueMap(ignoreNull, include,null != includeColumns ? Arrays.asList(includeColumns) : Collections.<String>emptySet());
	}
	@Override
	public Map<String, Object> asNameValueMap(boolean ignoreNull,final boolean include,Iterable<String>columns){
		Map<String, Object> map = asNameValueMap();
		if(ignoreNull){
			map = Maps.filterValues(map, Predicates.notNull());
		}
		if(null != columns){
			final Set<String> names = Sets.filter(Sets.newHashSet(columns),Predicates.notNull());
			map = Maps.filterKeys(map, new Predicate<String>() {
				@Override
				public boolean apply(String input) {
					return include ? names.contains(input) : !names.contains(input);
				}
			});
		}
		return Maps.newLinkedHashMap(map);
	}
	
	@Override
    public <B extends BaseBean> B copy(B bean)
    {
        return copy(bean,new int[0]);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <B extends BaseBean> B copy(B bean, int... fieldList)
    {   
    	if(bean != null && bean != this){
    		for (int columnId:metaData.validColumnIDsOrAll(fieldList)) {
    		    // valid column id only 
    		    if( bean.isInitialized(columnId) && !Objects.deepEquals(bean.getValue(columnId), getValue(columnId))){
    		        setValue(columnId, bean.getValue(columnId));
    		    }
    		}
    	}
        return (B)this;
    }

    @Override
    public <B extends BaseBean> B copy(B bean, String... fieldList)
    {
        return copy(bean, null, fieldList);
    }
    @SuppressWarnings("unchecked")
    @Override
	public <B extends BaseBean> B copy(B bean, Predicate<Integer> fieldFilter,int... fieldList)
    {
        int[] columnIds = filterColumnIDs(fieldFilter,fieldList);
        if (null == columnIds || 0 == columnIds.length){
            return (B) this; 
        }
        return copy(bean, columnIds);
    }
    @SuppressWarnings("unchecked")
    @Override
    public <B extends BaseBean> B copy(B bean, Predicate<String> fieldFilter,String... fieldList)
    {
        int[] columnIds = filterColumnIDs(fieldFilter,fieldList);
        if (null == columnIds || 0 == columnIds.length){
            return (B) this; 
        }
        return copy(bean, columnIds);
    }

    @Override
	@SuppressWarnings("unchecked")
	public <B extends BaseBean,F extends BaseBean> B copy(F from,Map<Integer,Integer> columnsMap){
        for(Map.Entry<Integer, Integer> entry : checkNotNull(columnsMap,"columnsMap is null").entrySet()){
        	setValue(entry.getValue(), null == from ? null : from.getValue(entry.getKey()));
        }
    	return (B) this;
    }
    
    @SuppressWarnings("unchecked")
	public <B extends BaseBean> B copy(Map<Integer, Object>values){
    	if(null != values){
    		for(Entry<Integer, Object> entry : values.entrySet()){
    			setValue(entry.getKey().intValue(), entry.getValue());
    		}
    	}
    	return (B) this;
    }
    @Override
    public boolean equalColumn(Object object,int columnId)
    {   
        if(!metaData.beanType.isInstance(object)){
            return false;
        }        
        BaseBean bean = (BaseBean)object; 
        if(null != bean){
            if(bean != this){
                if(metaData.isValidColumnID(columnId)){
                    // valid column id only 
                    if( bean.isInitialized(columnId) && !Objects.deepEquals(bean.getValue(columnId), getValue(columnId))){
                        return false;
                    }
                }
            }
            return true;
        }
        return false;
    }
    @Override
    public boolean equalColumn(Object object, int... fieldList)
    {   
        if(metaData.beanType.isInstance(object)){
            BaseBean bean = (BaseBean)object;
            if(bean != this && null != fieldList){
                for (int columnId:fieldList) {
                    // valid column id only 
                    if(metaData.isValidColumnID(columnId)){
                        if( bean.isInitialized(columnId) && !Objects.deepEquals(bean.getValue(columnId), getValue(columnId))){
                            return false;
                        }
                    }
                }
            }
            return true;
        }
        return false;
    }
    @Override
    public boolean equalColumn(Object object, Predicate<Integer> fieldFilter,int... fieldList)
    {
        return equalColumn(object, filterColumnIDs(fieldFilter,fieldList));
    }
    @Override
    public boolean equalColumn(Object object, Predicate<String> fieldFilter,String... fieldList)
    {
        return equalColumn(object, filterColumnIDs(fieldFilter,fieldList));
    }
    private int[] filterColumnIDs(Predicate<Integer> fieldFilter,int... fieldList){
        if(null != fieldList){
            if(null != fieldFilter){
                Iterable<Integer> columnIDs = Iterables.filter(Ints.asList(fieldList),fieldFilter);
                return Ints.toArray(Lists.newArrayList(columnIDs));
            }else {
                return fieldList;
            }
        }
        return null;
    }
    private int[] filterColumnIDs(Predicate<String> fieldFilter,String... fieldList)
    {
        if(null != fieldList){
            Iterable<String> filtered = Iterables.filter(Arrays.asList(fieldList),Predicates.notNull());
            if(null != fieldFilter){
                filtered = Iterables.filter(filtered,fieldFilter);
            }
            Iterable<Integer> columnIDs = Iterables.transform(filtered, metaData.COLUMNID_FUN);
            return Ints.toArray(Lists.newArrayList(columnIDs));
        }
        return null;
    }

    @Override
	public final String tableName() {
		return metaData.tablename;
	}

	/**
     * cast byte array to HEX string
     * 
     * @param input
     * @return {@code null} if {@code input} is null
     */
    private static final String toHex(byte[] input) {
        if (null == input){
            return null;
        }
        StringBuffer sb = new StringBuffer(input.length * 2);
        for (int i = 0; i < input.length; i++) {
            sb.append(Character.forDigit((input[i] & 240) >> 4, 16));
            sb.append(Character.forDigit(input[i] & 15, 16));
        }
        return sb.toString();
    }
    private static final StringBuilder append(StringBuilder buffer,boolean full,byte[] value){
        if(full || null == value){
            buffer.append(toHex(value));
        }else{
            buffer.append(value.length).append(" bytes");
        }
        return buffer;
    }
    private static int stringLimit = 64;
    private static final int MINIMUM_LIMIT = 16;
    private static final StringBuilder append(StringBuilder buffer,boolean full,String value){
        if(full || null == value || value.length() <= stringLimit){
            buffer.append(value);
        }else{
            buffer.append(value.substring(0,stringLimit - 8)).append(" ...").append(value.substring(stringLimit-4,stringLimit));
        }
        return buffer;
    }
    private static final StringBuilder append(StringBuilder buffer,boolean full,Object value){
    	if(value instanceof String){
    		return append(buffer, full, (String)value);
    	}
    	if(value instanceof byte[]){
    		return append(buffer, full, (byte[])value);
    	}
        return buffer.append(value);
    }
    public static final void setStringLimit(int limit){
        checkArgument(limit >= MINIMUM_LIMIT, "INVALID limit %s,minimum value %s",limit,MINIMUM_LIMIT);
        stringLimit = limit;
    }
	@Override
	public String toString(boolean notNull, boolean fullIfStringOrBytes) {
        StringBuilder builder = new StringBuilder(this.getClass().getName()).append("@").append(Integer.toHexString(this.hashCode())).append("[");
        int count = 0;
        for(int i = 0; i < metaData.columnCount; ++i){
        	Object value = getValue(i);
        	if( !notNull || null != value){
            	if(count > 0){
            		builder.append(",");
            	}
        		builder.append(metaData.columnNameOf(i)).append("=");
        		append(builder,fullIfStringOrBytes,value);
        		++count;
        	}
        }
        builder.append("]");        
		return builder.toString();
	}
	@Override
	public boolean equals(Object object) {
		if(!metaData.beanType.isInstance(object)){
			return false;
		}
		BaseBean bean = (BaseBean)object;
		EqualsBuilder equalsBuilder = new EqualsBuilder();
		for(int i=0; i<metaData.columnCount; ++i){
			equalsBuilder.append((Object)getValue(i), (Object)bean.getValue(i));
		}
		return equalsBuilder.isEquals();
    }
	@Override
    public int hashCode() {
		HashCodeBuilder hashCodeBuilder = new HashCodeBuilder(-82280557, -700257973);
		int[] hashIds = metaData.primaryKeyCount > 0 ? metaData.primaryKeyIds : metaData.defaultColumnIdList;
		for(int i=0; i<hashIds.length; ++i){
			hashCodeBuilder.append((Object)getValue(hashIds[i]));
		}
		return hashCodeBuilder.toHashCode();
    }
	
    @Override
    public String toString() {
        return toString(true,false);
    }

    @Override
    public int compareTo(BaseRow object){
    	CompareToBuilder compareToBuilder = new CompareToBuilder();
    	for(int i=0; i<metaData.columnCount; ++i){
    		compareToBuilder.append((Object)getValue(i), (Object)object.getValue(i));
    	}
    	return compareToBuilder.toComparison();
    }
    @Override
    public BaseRow clone(){
		try {
		    BaseRow bean= (BaseRow) super.clone();
		    for (int columnId = 0; columnId < metaData.columnCount; ++columnId) {
		        // initialized column only 
		        if( isInitialized(columnId)){
		            Object value = getValue(columnId);
		            if(value instanceof Cloneable){
		                value = value.getClass().getMethod("clone").invoke(value);
		            }
		            bean.setValue(columnId, value);
		        }
		    }
		    bean.setInitialized(getInitialized());
		    bean.setModified(getModified());
		    return bean;
		} catch (CloneNotSupportedException e) {
		 // this shouldn't happen, since we are Cloneable
            throw new InternalError(e.getClass().getName() + ":" + e.getMessage());
		} catch (Exception e) {
            Throwables.throwIfUnchecked(e);
            throw new RuntimeException(e);
        }
	}
	/**
	 * return {@link RowMetaData} instance for current bean
	 * @since 3.9.0
	 */
	public RowMetaData fetchMetaData() {
		return metaData;
	}

	/**
	 * truncate String,binary field 
	 * @param columnID
	 * @since 3.17.7
	 */
	public void truncate(int columnID) {
	    if(null != metaData.columnSizes && metaData.isValidColumnID(columnID)) {
	        int limit = metaData.columnSizes[columnID];
	        if(isModified(columnID)) {
	            Object value = getValue(columnID);
	            if(value instanceof String) {
	                // ignore JSON field
	                if(String.class.equals(metaData.fieldOf(columnID).getType())){
	                    String s = (String)value;
	                    byte[] bytes = s.getBytes();
	                    if(bytes.length > limit){
	                        setValue(columnID, new String(bytes, 0, limit));
	                    }
	                }
	            }else if(value instanceof byte[]) {
	                byte[] bytes = (byte[])value;
	                if(bytes.length > limit){
	                    setValue(columnID, Arrays.copyOf(bytes,limit));
	                }
	            }else if(value instanceof ByteBuffer) {
	                byte[] bytes = Sql2javaSupport.getBytesInBuffer((ByteBuffer) value);
	                if(bytes.length > limit){
                        setValue(columnID, ByteBuffer.wrap(Arrays.copyOf(bytes,limit)));
                    }
	            }
	        }
	    }
	}
	/**
     * truncate String,binary field 
     * @param column column name or field name
     * @since 3.17.7
     */
	public void truncate(String column) {
	    truncate(metaData.columnIDOf(column));
	}
}
